package pl.edu.agh.jemo.gui.chart;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import pl.edu.agh.jemo.evolution.population.Population;
import pl.edu.agh.jemo.gui.LoggerHelper;

/**
 * Handles creation of all algorithm output. Prepares result folder with charts and population lists from every step.
 * Creates logs from algorithm run, and any other type of output generated by algorithm.
 * This class is singleton, which allows it to be accesible from every point of algorithm, without creating new object.
 * On the other hand, class is designed to be used with abstract algorithm, it's usage is connected with abstract algorithm sequence and plotter shouldn't be used separatly.
 * 
 * @author Marcin Zbijowski
 *
 */
public class Plotter {
	/**
	 * Instance of plotter used by algorithm.
	 */
	private static Plotter plotter = null;
	/**
	 * Number of currently ploted step.
	 */
	private int step = 0;
	/**
	 * List of plots requested for current step.
	 */
	private ArrayList<Plot> plots;
	/**
	 * Absolute path to current output directory.
	 */
	private String path;
	/**
	 * String formula in gnuplot readable format, used as a background for genotype plot.
	 */
	private String bgFormula = "";
	/**
	 * Writer used to create script generating graphs at the end of algorithm.
	 */
	private BufferedWriter scriptWriter;
	/**
	 * Writer used to save metrics values to output file.
	 */
	private BufferedWriter metricsWriter;
	/**
	 * Genotype boundaries used to determine scale of genotype plot.
	 */
	private Double[][] genotypeBoundaries;
	/**
	 * Phenotype boundaries used to determine scale of genotype plot.
	 */
	private Double[][] phenotypeBoundaries;	
	/**
	 * Number of genotype dimensions. Only for 2 dimensions plots can be created.
	 */
	private int dimensions;
	/**
	 * Absolute path to current output directory.
	 */
	private String outputPath;
	
	/**
	 * Returns created plotter instance.
	 * If plotter was not yet created, creates new plotter.
	 * 
	 * @return Instance of ready to use plotter.
	 * @throws IOException if plotter was unable to generate output path.
	 */
	public static Plotter getPlotter() throws IOException {
		if (plotter == null) {
			plotter = new Plotter();
		}
		return plotter;
	}

	/**
	 * Returns created plotter instance.
	 * If plotter was not yet created, creates new plotter.
	 * Additionally initializes plotter boundaries. These values are required to generate charts on plotter release, however plots might be added earlier.
	 * 
	 * @param boundaries New boundaries to be used by plotter.
	 * @param dimensions New dimensions to be used by plotter.
	 * @return Instance of ready to use plotter.
	 * @throws IOException if plotter was unable to generate output path.
	 */
	public static Plotter getPlotter(Double[][] boundaries, int dimensions) throws IOException {
		if (plotter == null) {
			plotter = new Plotter();
		}
		plotter.genotypeBoundaries = boundaries;
		plotter.dimensions = dimensions;
		return plotter;
	}

	/**
	 * Creates plotter and initializes it's output.
	 * Results are created in directory which name is taken from current date. Additionally creates two output files. One for scripts generating plots, and another for metrics values.
	 * 
	 * @throws IOException if constructor fails to create any of output files.
	 */
	private Plotter() throws IOException {
		// Create output directory
		DateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd-HH_mm_ss");
		Calendar cal = Calendar.getInstance();
		new File(".\\output").mkdir();
		String dirName = dateFormat.format(cal.getTime());
		outputPath = ".\\output\\" + dirName;
		File dir = new File(outputPath);
		outputPath += "\\";
		dir.mkdir();
		// Set not relative path for charts
		path = dir.getCanonicalPath() + "\\";
		scriptWriter = new BufferedWriter(new FileWriter(new File(path + "script.plot")));
		metricsWriter = new BufferedWriter(new FileWriter(new File(path + "metrics.txt")));

		plots = new ArrayList<Plot>();
		
		LoggerHelper.getInstance().addFileAppender(path+"log.log");
	}
	
	private Plotter(String additionalPath) throws IOException {
		// Create output directory
		new File(".\\output").mkdir();
		new File(".\\output" + additionalPath).mkdir();
		outputPath = ".\\output" + additionalPath;
		File dir = new File(outputPath);
		outputPath += "\\";
		dir.mkdir();
		// Set not relative path for charts
		path = dir.getCanonicalPath() + "\\";
		System.out.println(path);
		scriptWriter = new BufferedWriter(new FileWriter(new File(path + "script.plot")));
		metricsWriter = new BufferedWriter(new FileWriter(new File(path + "metrics.txt")));

		plots = new ArrayList<Plot>();
		
		LoggerHelper.getInstance().addFileAppender(path+"log.log");
	}
	
	/**
	 * Returns output path for current plotter instance.
	 * 
	 * @return String value of path used as an output by plotter.
	 */
	public String getOutputPath() {
		return outputPath;
	}
	
	/**
	 * Adds plot request to be processed in current step.
	 * 
	 * @param plot Prepared plot object with data to be plotted.
	 */
	public void addPlot(Plot plot) {
		List<Population> pops = plot.getPopulations();
		for (int i = 0 ; i < pops.size() ; i++) {
			if (pops.get(i).size() == 0) {
				pops.remove(i--);
			}
		}
		if (pops.size() == 0) {
			return;
		}
		plots.add(plot);
	}
	
	/**
	 * Adds metric value in current step.
	 * 
	 * @param value Value of the metric.
	 * @param name String representation identifying metric.
	 */
	public void addMetric(BigDecimal value, String name) {
		try {
			metricsWriter.write("\t" + name + "\t" + value + "\n");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Creates output file with content of given population. Exact content of file is defined by given parameters.
	 * For each population new file is created. It is created in folder connected with plot name, and current step number.
	 * Name of output file contains index of population plotted in one step in single plot.
	 * 
	 * @param path Output path, where population points should be stored.
	 * @param population Population which points should be saved to file.
	 * @param phenotype If true, then population phenotypes are saved, if false writes population genotypes.
	 * @throws IOException if population output path was not created properly.
	 */
	private void savePopulationPoints(String path, Population population, boolean phenotype) throws IOException {
		if (population == null) {
			return;
		}
	    BufferedWriter out = new BufferedWriter(new FileWriter(path));
	    for (int i = 0 ; i < population.size() ; i++ ) {
    		StringBuilder sb = new StringBuilder();
	    	if (phenotype) {
	    		for (int j = 0 ; j < population.get(i).getPhenotype().length ; j++ ) {
	    			sb.append(population.get(i).getPhenotype()[j]);
	    			if (j < population.get(i).getPhenotype().length - 1) {
	    				sb.append(" ");
	    			}
	    		}
	    		sb.append("\n");
	    		out.write(sb.toString());
	    	} else {
	    		for (int j = 0 ; j < population.get(i).getGenotype().length ; j++ ) {
	    			sb.append(population.get(i).getGenotype()[j].asDouble());
	    			if (j < population.get(i).getGenotype().length - 1) {
	    				sb.append(" ");
	    			}
	    		}
	    		sb.append("\n");
	    		out.write(sb.toString());
	    	}
		}
	    out.close();

	}
	
	/**
	 * Prepares string containing gnuplot script parameters.
	 * Parameters returned here are common for every created plot.
	 * Some of parameters values generated here depend on problem boundaries, so this value must be set before this method is called.
	 * 
	 * @param output Output file for generated chart.
	 * @param genotype If this is true, genotype chart is created, otherwise pareto front is drawn. 
	 * @param count Number of populations in one plot.
	 * @return String filled with parameters used for gnuplot script.
	 */
	private String getScriptParameters(String output, boolean genotype, int count) {
		StringBuilder sb = new StringBuilder();
	    sb.append("set terminal png size 1280, 1024 crop\n");
	    if (genotype) {
		    sb.append(String.format("set xrange [%f:%f]\n", genotypeBoundaries[0][0], genotypeBoundaries[0][1]).replace(',', '.'));
		    sb.append(String.format("set yrange [%f:%f]\n", genotypeBoundaries[1][0], genotypeBoundaries[1][1]).replace(',', '.'));
	    } else {
	    	if (this.phenotypeBoundaries != null) {
	    		sb.append(String.format("set xrange [%f:%f]\n", phenotypeBoundaries[0][0], phenotypeBoundaries[0][1]).replace(',', '.'));
			    sb.append(String.format("set yrange [%f:%f]\n", phenotypeBoundaries[1][0], phenotypeBoundaries[1][1]).replace(',', '.'));
	    	} else {
	    		sb.append("set autoscale\n");
	    	}
	    }

    	sb.append("set multiplot\n");

    	sb.append("unset key\n");
    	sb.append("set view 0,0\n");
    	sb.append("set isosamples 100,100\n");
    	sb.append("unset surface\n");
    	sb.append("set cntrparam levels auto 30\n");
    	sb.append("set cntrparam bspline\n");
    	sb.append("set contour base\n");
    	sb.append("unset ztics\n");
	    
	    if ((genotype) && (count == 1)) {
	    	sb.append("splot " + bgFormula + " title \"obj func\"\n");
	    }
	    
	    sb.append("set surface\n");
	    sb.append("unset contour\n");
	    sb.append("unset ztics\n");
	    sb.append("unset key\n");
	    
	    StringBuilder source = new StringBuilder();
	    for (int i = 0 ; i < count ; i++) {
	    	source.append(String.format("\'%s%s\\p%d.txt\'", path, output, i));
	    	if ((i + 1) < count) {
	    		source.append(", ");
	    	}
	    }
	    
	    if ((genotype) && (count == 1)) {
	    	sb.append(String.format("splot %s using 1:2:2 title \"specimen\" with points pointtype 7\n", source.toString()));
	    }
	    else {
	    	sb.append(String.format("plot %s using 1:2\n", source.toString()));
	    }
	    sb.append("unset multiplot\n");	
		return sb.toString();
	}
	
	/**
	 * Fills gnuplot script with data needed to plot population charts.
	 * Uses getScriptParameters for detailed output configuration. This method is applicable only for two dimensional problems.
	 * 
	 * @param output Path where created graph should be saved. 
	 * @param genotype If true generates genotype chart, otherwise phenotype chart is created.
	 * @param count Number of populations in one plot.
	 * @return String with complete gnuplot script to plot charts.
	 */
	private String createPlotScript(String output, boolean genotype, int count) {
		StringBuilder sb = new StringBuilder();
		sb.append(String.format("set output \'%s%s\\graph.png\'\n", path, output));
		sb.append(getScriptParameters(output, genotype, count));
		return sb.toString();
	}
	
	/**
	 * Creates copy of charts in one folder allowing user to watch them manually using system picture browser.
	 * 
	 * @param output Output directory in which data for charts can be found.
	 * @param genotype If true generates genotype chart, otherwise phenotype chart is created.
	 * @param count Number of populations in one plot.
	 * @param humanOutput Path to output directory where plots should be saved, and are easily accessible by user.
	 * @return String with complete gnuplot script to plot charts.
	 */
	private String createHumanPlotScript(String output, boolean genotype, int count, String humanOutput) {
		StringBuilder sb = new StringBuilder();
		if (genotype) {
			sb.append(String.format("set output \'%s%s\\g%d.png\'\n", path, humanOutput, step));
		} else {
			sb.append(String.format("set output \'%s%s\\p%d.png\'\n", path, humanOutput, step));
		}
		sb.append(getScriptParameters(output, genotype, count));
		return sb.toString();
	}

	/**
	 * Writes values of populations in plot to files and prepares gnuplot script for that plot.
	 * All versions of plots are prepared here, both human, and application readable.
	 * 
	 * @param plot Reference to plot object that needs to be saved.
	 * @throws IOException if method fails to create any of output files.
	 */
	private void savePlot(Plot plot) throws IOException {
		// Empty population cannot be plotted
		if ((plot.getPopulations().size() == 0) || (plot.getPopulations().get(0) == null) || (plot.getPopulations().get(0).size() == 0)) {
			return;
		}
		File plotDir = new File(path + plot.getName() + "\\" + String.valueOf(step));
		plotDir.mkdirs();
		if (plot.getPlotType() == PlotType.BOTH) {
			plotDir = new File(path + plot.getName() + "\\" + String.valueOf(step) + "\\genotype");
			plotDir.mkdirs();
			plotDir = new File(path + plot.getName() + "\\" + String.valueOf(step) + "\\phenotype");
			plotDir.mkdirs();
			for (int i = 0 ; i < plot.getPopulations().size() ; i++ ) {
				savePopulationPoints(path + plot.getName()+ "\\" + String.valueOf(step) + "\\genotype\\p" + i + ".txt", plot.getPopulations().get(i), false);
				savePopulationPoints(path + plot.getName()+ "\\" + String.valueOf(step) + "\\phenotype\\p" + i + ".txt", plot.getPopulations().get(i), true);
				
			}
			new SinglePopulationPlotter(path + plot.getName()+ "\\" + String.valueOf(step) + "\\genotype");
			if (dimensions == 2) {
				scriptWriter.write(createPlotScript(plot.getName() + "\\" + String.valueOf(step) + "\\genotype", true, plot.getPopulations().size()));
				scriptWriter.write(createPlotScript(plot.getName() + "\\" + String.valueOf(step) + "\\phenotype", false, plot.getPopulations().size()));
				scriptWriter.write(createHumanPlotScript(plot.getName() + "\\" + String.valueOf(step) + "\\genotype", true, plot.getPopulations().size(),plot.getName()));
				scriptWriter.write(createHumanPlotScript(plot.getName() + "\\" + String.valueOf(step) + "\\phenotype", false, plot.getPopulations().size(),plot.getName()));
			}
		} else {
			String plotType = "\\";
			if (plot.getPlotType() == PlotType.GENOTYPE) {
				plotType += "genotype";
			} else {
				plotType += "phenotype";
			}
			plotDir = new File(path + plot.getName() + "\\" + String.valueOf(step) + plotType);
			plotDir.mkdirs();
			for (int i = 0 ; i < plot.getPopulations().size() ; i++ ) {
				savePopulationPoints(path + plot.getName() + "\\" + String.valueOf(step) + plotType + "\\p" + i + ".txt", plot.getPopulations().get(i), plot.getPlotType() != PlotType.GENOTYPE);
			}				
			new SinglePopulationPlotter(path + plot.getName() + "\\" + String.valueOf(step) + plotType);
			if (dimensions == 2) {
				scriptWriter.write(createPlotScript(plot.getName() + "\\" + String.valueOf(step) + plotType, plot.getPlotType() == PlotType.GENOTYPE, plot.getPopulations().size()));
				scriptWriter.write(createHumanPlotScript(plot.getName() + "\\" + String.valueOf(step) + plotType, plot.getPlotType() == PlotType.GENOTYPE, plot.getPopulations().size(),plot.getName()));
			}
		}
		
	}
	
	/**
	 * Writes all populations saved for given step, into output file.
	 * Prepares gnuplot scripts for these populations. Also writes metrics values to another output file.
	 * This method is called by abstract algorithm after every step. And should not be called manually.
	 * 
	 * @throws IOException if method fails to create any of output files.
	 */
	public void endStep() throws IOException {
		int unnamedCount = 0;
		for (Plot plot : plots) {
			if (plot.getName() == "") {
				StringBuilder sb = new StringBuilder();
				sb.append("unnamed");
				sb.append(unnamedCount++);
				plot.setName(sb.toString());
			}
			savePlot(plot);
		}
		
		step++;
		
		metricsWriter.write(String.format("Step %d\n",step));
		plots.clear();
	}
	
	/**
	 * Ends plotter existence, and tries to use gnuplot to prepare charts based on generated scripts.
	 * This method closes all opened files and destroys plotter reference.
	 * It is automatically called after algorithm ends, and should not be called anywhere else. Calling another methods after that one is executed may result with undefined behavior and errors.
	 * 
	 * @throws IOException if method fails to create any of output files.
	 */
	public void releasePlotter() throws IOException {
		scriptWriter.close();
		metricsWriter.close();

		Runtime runtime = Runtime.getRuntime();
		String[] cmd = {"gnuplot",path + "script.plot"};
		Process gnuplot = runtime.exec(cmd);
		try {
			gnuplot.waitFor();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(path);
		String[] cmd2 = {"explorer", path};

		runtime.exec(cmd2);

		plotter = null;
	}

	/**
	 * Sets string format gnuplot formula for objective function in problem. If more than one objective function was added, uses first formula found.
	 * 
	 * @param bgFormula New value of formula in gnuplot string form.
	 */
	public void setBgFormula(String bgFormula) {
		this.bgFormula = bgFormula;
	}
	
	/**
	 * Returns value of currently set gnuplot formula.
	 * 
	 * @return Value of gnuplot formula parameter.
	 */
	public String getBgFormula() {
		return bgFormula;
	}
	
	public static Plotter getPlotter(Double[][] boundaries, Double[][] phenotypeBoundaries, int dimensions, String additionalPath) throws IOException {
		if (plotter == null) {
			plotter = new Plotter(additionalPath);
		}
		plotter.genotypeBoundaries = boundaries;
		plotter.phenotypeBoundaries = phenotypeBoundaries;
		plotter.dimensions = dimensions;
		return plotter;
	}
}
